iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0
AI/ ML & Data

從零開始學AI:數學基礎與程式碼撰寫全攻略系列 第 8

【Day 8】使用Pytorch實現深度神經網絡進行MNIST手寫數字辨識

  • 分享至 

  • xImage
  •  

前言

現在你已經了解了什麼是深度神經網路,所以今天我們主要學習如何使用 Pytorch 來完成前幾天所講的前向傳播和反向傳播方法。我們將使用MNIST這個手寫辨識資料集,進行模型的訓練和預測,讓你瞭解實際上的深度學習運算過程。

使用Pytorch建立深度神經網路

這次我們同樣使用監督式學習和深度神經網絡來解決 MNIST 手寫數字辨識問題。我會從下載數據集、定義模型、訓練模型到最終進行測試,詳細告訴你如何在 Pytorch 中實現。現在讓我們來看看以下步驟。

【STEP 1】下載資料集並進行資料前處理

首先我們需要import這次程式所必須的 Pytorch 函式庫,而由於我們將進行圖像辨識,因此我們會使用torchvision及其中的transforms來對圖像進行處理。

import torch
import torch.nn as nn       # 建立神經網路用
import torch.optim as optim # 建立優化器用
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import random

接下來我們需要進行資料前處理 (Data Preprocessing)的步驟,這個步驟主要包括兩個部分。首先,將我們的陣列 (List)轉換成張量 (Tensor)類型。其次,對圖像進行正規化 (Normalization)處理。使用正規化的原因是,若輸入的資料數值過高,容易導致模型的梯度和損失值也變得很高,進而使每次的權重變動變得更加不可控制,讓模型更難收斂。因此,在資料前處理時,需要先完成這兩個重要的步驟。

張量是一種多維陣列,廣泛應用於深度學習中的資料處理,特別是在神經網路訓練過程中使用。張量可以是標量 (0 維)、向量 (1 維)、矩陣 (2 維) 及更高維度的資料結構。

# 數據預處理
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

接下來我們通過以下程式碼將定義好的 transform 作用在 MNIST 數據集上,同時我們也設定其超參數 download=True 開啟,這樣 我們就能夠快速地對這些照片進行資料前處理並下載。

在這裡我們先不多說datasetsDataLoader這兩個類別的概念,其概念將會再後續章節進行詳細的講解。

# 下載 MNIST 數據集
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)

train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)

同時我們設定批量數為 64 張圖片,使模型可以正常運算。我們之所以不像前幾天那樣一次把所有資料當作批量給模型訓練,是因為 MNIST 手寫辨識集在訓練集上有 6 萬張圖片,測試集則有 1 萬張,對於一張 GPU 而言是無法一次容納下這麼多資料的。

【STEP 2】查看資料大小與型態

而當我們在處理資料時,最需要理解的就是這些資料的維度,如果我們輸入錯誤的維度,模型就無法運作。這一點在單層感知器章節中已經提到過了,因為它太重要了所以我必須再重複一次。現在我們可以先通過以下的程式碼來取得訓練資料集的圖片及其對應的標籤,並顯示出它們的維度。

x_train = train_dataset.data    # 圖片
y_train = train_dataset.targets # 標籤
print(f'x_train size: {x_train.size()}')
print(f'y_train size: {y_train.size()}')
# -----輸出-----
x_train size: torch.Size([60000, 28, 28])
y_train size: torch.Size([60000])

從以上結果可以看到,訓練資料包含 60000 張圖片,每張圖片的大小為 28×28 像素。為了更直觀地理解這些圖片,可以使用 matplotlib 將它們可視化。在圖片中越接近白色的區域,數值越接近 255;越接近黑色的區域,數值則越接近 0,範圍介於 0 到 255 之間。由於數值較大,因此這也是我們在第一步驟所進行正規化的原因。

fig, axs = plt.subplots(1, 6, figsize=(15, 3))
for i in range(6):
    idx = random.randint(0, len(x_train) - 1)
    img, label = x_train[idx], y_train[idx]
    axs[i].imshow(img, cmap='gray')
    axs[i].set_title(f'Label: {label}')
    axs[i].axis('off')
plt.show()

https://ithelp.ithome.com.tw/upload/images/20240922/20152236niCiQaGAUk.png

【STEP 3】定義深度神經網路模型

在Pytorch中,需要繼承nn.Module來使用其相關的方法。不過其概念非常簡單,我們通常會在init方法中定義模型的結構與激勵函數。因此,若我們要定義一個有四層隱藏層的模型,可以這樣定義:

class DNN(nn.Module):
        def __init__(self, input_shape, output_shape):
            super(DNN, self).__init__()
            self.fc1 = nn.Linear(input_shape, 512)  # 輸入->隱藏
            self.fc2 = nn.Linear(512, 256)          # 隱藏->隱藏
            self.fc3 = nn.Linear(256, 128)          # 隱藏->隱藏
            self.fc4 = nn.Linear(128, output_shape) # 隱藏->輸出
            self.relu = nn.ReLU()  # 激勵函數

在定義模型的前向傳播過程時,有一個需要特別注意的地方:由於深度神經網路只能處理一維的輸入資料,因此我們需要將輸入的28x28圖片展平為784維的一維向量。為了實現這一點在輸入的第一層,我們會使用view函數來進行轉換操作。這樣可以確保網路能夠正確接收並處理資料。

view 函數中使用 -1 來表示自適應維度,這是因為我們不確定輸入的批次大小,但我們知道每個輸入的特徵數量是 784。因此我們可以將資料從形狀為 (batch_size, 28, 28) 轉換為 (batch_size, 784)。其中,batch_size 的維度使用 -1,可以自動計算並適應當前的批次大小。

        def forward(self, x):
            x = x.view(-1, 28 * 28)
            x = self.relu(self.fc1(x))
            x = self.relu(self.fc2(x))
            x = self.relu(self.fc3(x))
            x = self.fc4(x)
            return x

在這裡我們使用了 ReLU(Rectified Linear Unit)作為激勵函數。其數學原理相對簡單:當輸入值小於0時,輸出為0;當輸入值大於或等於0時,輸出保持不變。ReLU 是隱藏層中最常使用的激勵函數之一,甚至像 ChatGPT 這類的語言模型也使用了基於 ReLU 的變體。因此本次教學中,我們將以 ReLU 作為激勵函數的範例來進行。

【STEP 4】訓練模型

在訓練模型時,雖然與前幾天的內容相似,但這裡我們仍需注意一些基本事項。由於我們使用梯度追蹤來進行反向傳播,計算損失值後可以使用內建的 backward() 方法來求取梯度。優化器則需要接收模型中所有可調整的參數,這是因為在模型通過 backward() 計算梯度後,還需要使用 optimizer.step() 更新權重。因此在初始化模型和優化器時,我們可以這樣撰寫程式碼。

# 定義損失函數和優化器
    model = DNN(x_train.shape[1] * x_train[2], len(set(y_train))
    criterion = nn.CrossEntropyLoss()        # 計算分類任務時通常會使用CrossEntropyLoss
    optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam是一個通用性極高的優化器

在定義訓練過程時,還需特別注意 optimizer.zero_grad() 。原因在於每當我們追蹤梯度時,梯度會在每個批次中累加。如果不清除前一個批次的梯度,新的批次計算就會受到前一批次的影響。因此使用 optimizer.zero_grad() 的目的,是確保在計算當前批次時,將其視為一次獨立的計算,不受之前批次的影響。

def train_model(model, train_loader, criterion, optimizer, num_epochs=5):
    for epoch in range(num_epochs):
        running_loss = 0.0
        for i, (inputs, labels) in enumerate(train_loader):
            # 清空梯度
            optimizer.zero_grad()

            # 前向傳播
            outputs = model(inputs)
            loss = criterion(outputs, labels)

            # 反向傳播和優化
            loss.backward()
            optimizer.step()

            # 累計損失
            running_loss += loss.item() # item()張量轉換成純量
            if i % 100 == 99:
                print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')
                running_loss = 0.0
    print('Finished Training')
    
train_model(model, train_loader, criterion, optimizer, num_epochs=5)
# -----輸出-----
Epoch [5/5], Step [900/938], Loss: 0.0719

在該程式中我們可以清楚看到,訓練過程還是前向傳播、反向傳播、以及優化器更新權重這三個動作。並且在這裡我們也能關注損失值的變化。通常來說如果損失值無法降低到 0.X 的範圍,這可能意味著模型在該資料集上的表現不佳,或者資料集本身存在問題。這時可能需要檢查模型架構、資料集品質或訓練參數,來進一步改善結果。

【STEP 5】測試模型效能並進行預測

還記得我們下載資料時有一個測試資料集嗎?這個資料集的功能是用來驗證模型的實際效能。因為模型在訓練過程中已經看過訓練資料集,所以如果我們用訓練資料來驗證模型,結果可能會不準確。因此,我們通常會將資料進行分割,將部分資料保留,讓模型在訓練過程中無法接觸,並以此作為評估模型效能的依據。這個概念可以比喻為:訓練資料就像課本裡的習題,而測試資料則是考試的內容,用來評估學習效果。

def test_model(model, test_loader):
    correct = 0
    total = 0
    model.eval()  # 設置模型為推論模式
    with torch.no_grad():  # 禁用梯度計算
        for inputs, labels in test_loader:
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy of the model on the 10000 test images: {100 * correct / total:.2f}%')

test_model(model, test_loader)
# -----輸出-----
Accuracy of the model on the 10000 test images: 97.45%

在這裡,我們需要將模型切換到推論模式(inference mode),這樣在運算時可以固定模型中特定層所引入的隨機性變化(像是 dropout 這類層)。另外我們還可以使用 with torch.no_grad(),因為在測試或推論模型時,我們不需要計算反向傳播,這樣 PyTorch 就不會自動追蹤梯度變化,進而加快運算速度。

當模型訓練完畢並進行測試後,準確率甚至達到了 97%。接下來,若我們要進行實際預測,可以撰寫以下程式碼。但需要特別注意的是,在實際預測時必須使用與訓練時相同的資料前處理技術。若不遵守相同的處理步驟,則可能導致預測結果錯誤。

def predict_random_image(model, test_dataset):
    # 隨機選擇一張測試集圖片
    idx = random.randint(0, len(test_dataset) - 1)
    img, label = test_dataset[idx]
    img_reshaped = img.view(-1, 28 * 28)

    # 進行預測
    model.eval()
    with torch.no_grad():
        output = model(img_reshaped)
        _, predicted = torch.max(output.data, 1)

    # 繪製圖片並顯示預測結果
    plt.imshow(img.squeeze(), cmap='gray')
    plt.title(f'Ground Truth: {label}, Predicted: {predicted.item()}')
    plt.axis('off')
    plt.show()

https://ithelp.ithome.com.tw/upload/images/20240922/20152236pO7IXU02z9.png
這時我們已經發現模型能夠學會這些圖像的特徵,並進行預測了!

總結

從今天的內容中可以看到,在處理模型建立與反向傳播的部分比我們前幾日還要簡單許多了吧!而我們今天的內容都是在處理該如何繪製一個圖片、資料前處理與學習Pytorch的程式碼的一些程式規範,但在今天你可能接觸到了一些不同的激勵函數與損失函數導致你對這方面有點混亂,因此在明天我會將這些內容引入到內容中,讓你從數學方面更號的理解ReLU與CrossEntropyLoss究竟做了哪些事情。


上一篇
【Day 7】深度神經網路與多層感知器的差異解析及PyTorch安裝指南
下一篇
【Day 9】辨識圖像的神工利器-卷機神經網路數學證明
系列文
從零開始學AI:數學基礎與程式碼撰寫全攻略30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言